// Copyright (c) 2017-2024, University of Cincinnati, developed by Henry Schreiner
// under NSF AWARD 1414736 and by the respective contributors.
// All rights reserved.
//
// SPDX-License-Identifier: BSD-3-Clause

#include <turbo/flags/cli/formatter.h>
#include <algorithm>
#include <string>
#include <utility>
#include <vector>
#include <turbo/flags/reflection.h>
#include <turbo/strings/str_cat.h>
#include <turbo/strings/str_split.h>
#include <turbo/strings/str_join.h>

namespace turbo::cli {
    // [CLI11:formatter_inl_hpp:verbatim]
    std::string
    Formatter::make_group(std::string group, bool is_positional, std::vector<const Option *> opts) const {
        std::stringstream out;

        out << "\n" << group << ":\n";
        for (const Option *opt: opts) {
            out << make_option(opt, is_positional);
        }

        return out.str();
    }

    std::string Formatter::make_positionals(const App *app) const {
        std::vector<const Option *> opts =
                app->get_options([](const Option *opt) { return !opt->get_group().empty() && opt->get_positional(); });

        if (opts.empty())
            return {};

        return make_group(get_label("Positionals"), true, opts);
    }

    std::string Formatter::make_groups(const App *app, AppFormatMode mode) const {
        std::stringstream out;
        std::vector<std::string> groups = app->get_groups();
        // Options
        for (const std::string &group: groups) {
            std::vector<const Option *> opts = app->get_options([app, mode, &group](const Option *opt) {
                return opt->get_group() == group // Must be in the right group
                       && opt->nonpositional() // Must not be a positional
                       && (mode != AppFormatMode::Sub // If mode is Sub, then
                           || (app->get_help_ptr() != opt // Ignore help pointer
                               && app->get_help_all_ptr() != opt)); // Ignore help all pointer
            });
            if (!group.empty() && !opts.empty()) {
                out << make_group(group, false, opts);

                if (group != groups.back())
                    out << "\n";
            }
        }

        return out.str();
    }

    std::string Formatter::make_description(const App *app) const {
        std::string desc = app->get_description();
        auto min_options = app->get_require_option_min();
        auto max_options = app->get_require_option_max();
        if (app->get_required()) {
            desc += " " + get_label("REQUIRED") + " ";
        }
        if ((max_options == min_options) && (min_options > 0)) {
            if (min_options == 1) {
                desc += " \n[Exactly 1 of the following options is required]";
            } else {
                desc += " \n[Exactly " + std::to_string(min_options) + " options from the following list are required]";
            }
        } else if (max_options > 0) {
            if (min_options > 0) {
                desc += " \n[Between " + std::to_string(min_options) + " and " + std::to_string(max_options) +
                        " of the follow options are required]";
            } else {
                desc += " \n[At most " + std::to_string(max_options) + " of the following options are allowed]";
            }
        } else if (min_options > 0) {
            desc += " \n[At least " + std::to_string(min_options) + " of the following options are required]";
        }
        return (!desc.empty()) ? desc + "\n" : std::string{};
    }

    std::string Formatter::make_usage(const App *app, std::string name) const {
        std::string usage = app->get_usage();
        if (!usage.empty()) {
            return usage + "\n";
        }

        std::stringstream out;

        out << get_label("Usage") << ":" << (name.empty() ? "" : " ") << name;

        std::vector<std::string> groups = app->get_groups();

        // Print an Options badge if any options exist
        std::vector<const Option *> non_pos_options =
                app->get_options([](const Option *opt) { return opt->nonpositional(); });
        if (!non_pos_options.empty())
            out << " [" << get_label("OPTIONS") << "]";

        // Positionals need to be listed here
        std::vector<const Option *> positionals = app->get_options([](const Option *opt) {
            return opt->get_positional();
        });

        // Print out positionals if any are left
        if (!positionals.empty()) {
            // Convert to help names
            std::vector<std::string> positional_names(positionals.size());
            std::transform(positionals.begin(), positionals.end(), positional_names.begin(), [this](const Option *opt) {
                return make_option_usage(opt);
            });

            out << " " << detail::join(positional_names, " ");
        }

        // Add a marker if subcommands are expected or optional
        if (!app->get_subcommands(
                [](const turbo::cli::App *subc) { return ((!subc->get_disabled()) && (!subc->get_name().empty())); })
            .empty()) {
            out << " " << (app->get_require_subcommand_min() == 0 ? "[" : "")
                    << get_label(app->get_require_subcommand_max() < 2 || app->get_require_subcommand_min() > 1
                                     ? "SUBCOMMAND"
                                     : "SUBCOMMANDS")
                    << (app->get_require_subcommand_min() == 0 ? "]" : "");
        }

        out << '\n';

        return out.str();
    }

    std::string Formatter::make_footer(const App *app) const {
        std::string footer = app->get_footer();
        if (footer.empty()) {
            return std::string{};
        }
        return "\n" + footer + "\n";
    }

    std::string Formatter::make_help(const App *app, std::string name, AppFormatMode mode) const {
        // This immediately forwards to the make_expanded method. This is done this way so that subcommands can
        // have overridden formatters
        if (mode == AppFormatMode::Sub)
            return make_expanded(app);

        std::stringstream out;
        if ((app->get_name().empty()) && (app->get_parent() != nullptr)) {
            if (app->get_group() != "Subcommands") {
                out << app->get_group() << ':';
            }
        }

        out << make_description(app);
        out << make_usage(app, name);
        out << make_positionals(app);
        out << make_groups(app, mode);
        out << make_subcommands(app, mode);
        out << make_footer(app);

        return out.str();
    }

    std::string Formatter::make_subcommands(const App *app, AppFormatMode mode) const {
        std::stringstream out;

        std::vector<const App *> subcommands = app->get_subcommands({});

        // Make a list in definition order of the groups seen
        std::vector<std::string> subcmd_groups_seen;
        for (const App *com: subcommands) {
            if (com->get_name().empty()) {
                if (!com->get_group().empty()) {
                    out << make_expanded(com);
                }
                continue;
            }
            std::string group_key = com->get_group();
            if (!group_key.empty() &&
                std::find_if(subcmd_groups_seen.begin(), subcmd_groups_seen.end(), [&group_key](std::string a) {
                    return detail::to_lower(a) == detail::to_lower(group_key);
                }) == subcmd_groups_seen.end())
                subcmd_groups_seen.push_back(group_key);
        }

        // For each group, filter out and print subcommands
        for (const std::string &group: subcmd_groups_seen) {
            out << "\n" << group << ":\n";
            std::vector<const App *> subcommands_group = app->get_subcommands(
                [&group](const App *sub_app) {
                    return detail::to_lower(sub_app->get_group()) == detail::to_lower(group);
                });
            for (const App *new_com: subcommands_group) {
                if (new_com->get_name().empty())
                    continue;
                if (mode != AppFormatMode::All) {
                    out << make_subcommand(new_com);
                } else {
                    out << new_com->help(new_com->get_name(), AppFormatMode::Sub);
                    out << "\n";
                }
            }
        }

        return out.str();
    }

    std::string Formatter::make_subcommand(const App *sub) const {
        std::stringstream out;
        detail::format_help(out,
                            sub->get_display_name(true) + (sub->get_required() ? " " + get_label("REQUIRED") : ""),
                            sub->get_description(),
                            column_width_);
        return out.str();
    }

    std::string Formatter::make_expanded(const App *sub) const {
        std::stringstream out;
        out << sub->get_display_name(true) << "\n";

        out << make_description(sub);
        if (sub->get_name().empty() && !sub->get_aliases().empty()) {
            detail::format_aliases(out, sub->get_aliases(), column_width_ + 2);
        }
        out << make_positionals(sub);
        out << make_groups(sub, AppFormatMode::Sub);
        out << make_subcommands(sub, AppFormatMode::Sub);

        // Drop blank spaces
        std::string tmp = detail::find_and_replace(out.str(), "\n\n", "\n");
        tmp = tmp.substr(0, tmp.size() - 1); // Remove the final '\n'

        // Indent all but the first line (the name)
        return detail::find_and_replace(tmp, "\n", "\n  ") + "\n";
    }

    std::string Formatter::make_option_name(const Option *opt, bool is_positional) const {
        if (is_positional)
            return opt->get_name(true, false);

        return opt->get_name(false, true);
    }

    std::string Formatter::make_option_opts(const Option *opt) const {
        std::stringstream out;

        if (!opt->get_option_text().empty()) {
            out << " " << opt->get_option_text();
        } else {
            if (opt->get_type_size() != 0) {
                if (!opt->get_type_name().empty())
                    out << " " << get_label(opt->get_type_name());
                if (!opt->get_default_str().empty())
                    out << " [" << opt->get_default_str() << "] ";
                if (opt->get_expected_max() == detail::expected_max_vector_size)
                    out << " ...";
                else if (opt->get_expected_min() > 1)
                    out << " x " << opt->get_expected();

                if (opt->get_required())
                    out << " " << get_label("REQUIRED");
            }
            if (!opt->get_envname().empty())
                out << " (" << get_label("Env") << ":" << opt->get_envname() << ")";
            if (!opt->get_needs().empty()) {
                out << " " << get_label("Needs") << ":";
                for (const Option *op: opt->get_needs())
                    out << " " << op->get_name();
            }
            if (!opt->get_excludes().empty()) {
                out << " " << get_label("Excludes") << ":";
                for (const Option *op: opt->get_excludes())
                    out << " " << op->get_name();
            }
            if (opt->is_flags()) {
                out << " [FLAGS]";
            }
        }
        return out.str();
    }

    static std::string make_short_path(std::string_view path) {
        std::vector<std::string> paths = turbo::str_split(path, "/");
        std::string result;
        if (paths.empty()) {
            return "\"\"";
        }
        if (paths.size() >= 4) {
            auto size = paths.size();

            if (paths[size - 4] == paths[size - 3]) {
                return turbo::str_cat(".../", paths[size - 3], "/", paths[size - 2], "/", paths[size - 1]);
            }
            return turbo::str_cat(".../", paths[size - 4], "/", paths[size - 3], "/", paths[size - 2], "/",
                                  paths[size - 1]);
        }
        return turbo::str_cat(".../", turbo::str_join(paths, "/"));
    }

    static std::string get_flags_type(const turbo::CommandLineFlag *cmd) {
        if (cmd->is_of_type<std::string>()) {
            return "TEXT";
        }
        if (cmd->is_of_type<bool>()) {
            return "BOOL";
        }
        if (cmd->is_of_type<int>() ||
            cmd->is_of_type<long int>()) {
            return "INT";
        }
        if (cmd->is_of_type<float>()) {
            return "FLOAT";
        }

        if (cmd->is_of_type<double>()) {
            return "DOUBLE";
        }

        if (cmd->is_of_type<std::vector<bool> >()) {
            return "BOOL LIST";
        }

        if (cmd->is_of_type<std::vector<std::string> >()) {
            return "TEXT LIST";
        }

        if (cmd->is_of_type<std::vector<int> >() ||
            cmd->is_of_type<std::vector<long int> >()) {
            return "INT LIST";
        }
        if (cmd->is_of_type<std::vector<float> >()) {
            return "FLOAT LIST";
        }
        if (cmd->is_of_type<std::vector<double> >()) {
            return "DOUBLE LIST";
        }
        if (cmd->is_of_type<turbo::Duration>()) {
            return "DURATION";
        }
        if (cmd->is_of_type<turbo::Time>()) {
            return "TIME";
        }
        if (cmd->is_of_type<turbo::CivilSecond>()) {
            return "DATE";
        }
        return "UDF";
    }

    std::string Formatter::make_option_desc(const Option *opt) const {
        std::string out;
        std::string_view view = opt->get_description();
        static const size_t max_description_len = 100;
        bool first = true;
        if (opt->is_flags()) {
            auto name = opt->get_single_name();
            auto cmd = find_command_line_flag(name);
            if (cmd) {
                auto default_v = cmd->default_value();
                if (default_v.empty()) {
                    default_v = "\"\"";
                }
                auto rl = cmd->has_user_validator();
                out = turbo::str_cat("<", get_flags_type(cmd), "> default=", default_v, " reload online=",
                                     rl ? "yes" : "no", " file=",
                                     make_short_path(cmd->filename()));
                first = false;
            }
        }
        while (view.size() > max_description_len) {
            if (first) {
                first = false;
            } else {
                out.append("\n");
            }
            auto tmp = view.substr(0, max_description_len);
            auto pos = tmp.find_last_of(' ');
            if (pos == std::string::npos) {
                pos = max_description_len;
            }
            out.append(tmp.data(), pos);
            if (view.size() > pos + 1) {
                view = view.substr(pos + 1);
            } else {
                view = "";
            }
        }
        if (!first) {
            out.append("\n");
        }
        out.append(view.data());
        return out;
    }

    std::string Formatter::make_option_usage(const Option *opt) const {
        // Note that these are positionals usages
        std::stringstream out;
        out << make_option_name(opt, true);
        if (opt->get_expected_max() >= detail::expected_max_vector_size)
            out << "...";
        else if (opt->get_expected_max() > 1)
            out << "(" << opt->get_expected() << "x)";

        return opt->get_required() ? out.str() : "[" + out.str() + "]";
    }

    // [CLI11:formatter_inl_hpp:end]
} // namespace turbo::cli
